# SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR.e.V.)
# SPDX-FileContributor: Ferdinand Rewicki <ferdinand.rewicki@dlr.de>
#
# SPDX-License-Identifier: Apache-2.0

"""Synthetic EDEN ISS - ICS data generation."""

import random
from datetime import datetime, timedelta
from typing import Optional, Union

import mockseries
import numpy as np
from mockseries.noise import GaussianNoise, RedNoise
from mockseries.seasonality import DailySeasonality

# from mockseries.signal.composed_signal import ComposedSignal
from mockseries.transition import LambdaTransition
from mockseries.trend import FlatTrend, Switch
from mockseries.utils import datetime_range

from saai.utils import anomalies_from_labels


class BaseAnomaly(mockseries.trend.Switch):
    """Basic Anomaly Class."""

    P_UNALIGNED: float = 0.1

    ID: int
    TRANS_IN_MINUTES: int
    TRANS_OUT_MINUTES: int
    MINUTES_MIN: int
    MINUTES_MAX: int
    HOURS_MIN: int
    HOURS_MAX: int
    DURATION_MINUTES: int
    DURATION_VAR_COEFF: int
    SWITCH_VAL: int
    SWITCH_VAR_COEFF: int

    @classmethod
    def add_to_signal(
        cls, signal: mockseries.signal.signal.Signal, start_date: datetime
    ):
        """Create an Anomaly and add it to a given signal.

        Parameters
        ----------
        signal : mockseries.signal.signal.Signal
            The Signal the anomaly will be added to.
        start_date : datetime
            The start date and time of the anomaly.

        Returns
        -------
        signal : mockseries.signal.signal.Signal
            The modified signal
        label :  mockseries.trend.Switch
            The anomaly switch that was added.


        """
        start_minutes = (
            np.random.randint(cls.MINUTES_MIN, cls.MINUTES_MAX)
            if cls.MINUTES_MIN < cls.MINUTES_MAX
            else cls.MINUTES_MIN
        )
        start_hour = (
            np.random.randint(cls.HOURS_MIN, cls.HOURS_MAX)
            if cls.HOURS_MIN < cls.HOURS_MAX
            else cls.HOURS_MIN
        )
        start = start_date + timedelta(hours=start_hour, minutes=start_minutes)
        start = start.replace(second=0)

        duration = (
            cls.DURATION_MINUTES
            + cls.DURATION_VAR_COEFF * np.random.rand()
            - int(cls.DURATION_VAR_COEFF / 2)
        )
        switch_value = (
            cls.SWITCH_VAL
            + cls.SWITCH_VAR_COEFF * np.random.rand()
            - int(cls.SWITCH_VAR_COEFF / 2)
        )

        anomaly = cls(
            start=start, duration=timedelta(minutes=duration), switch_value=switch_value
        )

        if cls.TRANS_IN_MINUTES == 0 and cls.TRANS_OUT_MINUTES == 0:
            label = Switch(
                start_time=start - timedelta(minutes=5),
                stop_time=start + timedelta(minutes=duration + 5),
                switch_value=cls.ID,
            )
        else:
            label = Switch(
                start_time=start - timedelta(minutes=int(cls.TRANS_IN_MINUTES) / 8),
                stop_time=start
                + timedelta(minutes=duration + int(cls.TRANS_OUT_MINUTES / 2)),
                switch_value=cls.ID,
            )
        signal += anomaly

        return signal, label


class CooldownPeak(BaseAnomaly):
    """Cooldown Peak Anomaly."""

    ID: int = 6

    TRANS_IN_MINUTES = 20
    TRANS_OUT_MINUTES = 20

    MINUTES_MIN: int = 0
    MINUTES_MAX: int = 30
    HOURS_MIN: int = 22
    HOURS_MAX: int = 22
    DURATION_MINUTES: int = 20
    DURATION_VAR_COEFF: int = 2
    SWITCH_VAL: int = 5
    SWITCH_VAR_COEFF: int = 2

    def __init__(
        self,
        start: datetime,
        duration: timedelta,
        base_value: float = 0,
        switch_value: float = 5,
    ):
        super().__init__(
            start_time=start,
            stop_time=start + duration,
            base_value=base_value,
            switch_value=switch_value,
            transition=self._transition(),
        )

    # @staticmethod
    # def _sigmoid(x):
    #     if x == 0 or x == 1:
    #         return x
    #     return 1 / (1 + np.exp(-10 * (x - 0.5)))

    def _transition(self):
        def acceleration(x):
            return 1 - (1 - x) ** 4

        def deceleration(x):
            return 1 - (1 - x) ** 4

        # acceleration = lambda x: 1 - (1 - x) ** 4
        # deceleration = lambda x: 1 - (1 - x) ** 4

        transition = LambdaTransition(
            transition_window=timedelta(minutes=self.TRANS_IN_MINUTES),
            transition_function=acceleration,
            stop_window=timedelta(minutes=self.TRANS_OUT_MINUTES),
            stop_function=deceleration,
        )

        return transition


class NightPeakAnomaly(BaseAnomaly):
    """NightPeak Anomaly."""

    ID: int = 5

    TRANS_IN_MINUTES = 60
    TRANS_OUT_MINUTES = 60

    MINUTES_MIN: int = 0
    MINUTES_MAX: int = 40
    HOURS_MIN: int = 1
    HOURS_MAX: int = 1
    DURATION_MINUTES: int = 10
    DURATION_VAR_COEFF: int = 2
    SWITCH_VAL: int = 5
    SWITCH_VAR_COEFF: int = 2

    def __init__(
        self,
        start: datetime,
        duration: timedelta,
        base_value: float = 0,
        switch_value: float = 5,
    ):
        super().__init__(
            start_time=start,
            stop_time=start + duration,
            base_value=base_value,
            switch_value=switch_value,
            transition=self._transition(),
        )

    @staticmethod
    def _sigmoid(x):
        if x == 0 or x == 1:
            return x
        return 1 / (1 + np.exp(-10 * (x - 0.5)))

    def _transition(self):
        def acceleration(x):
            return self._sigmoid(x)

        def deceleration(x):
            return self._sigmoid(x)

        # acceleration = lambda x: self.sigmoid(x)
        # # deceleration = lambda x: 1 - (1-x)**4
        # deceleration = lambda x: self.sigmoid(x)

        transition = LambdaTransition(
            transition_window=timedelta(minutes=self.TRANS_IN_MINUTES),
            transition_function=acceleration,
            stop_window=timedelta(minutes=self.TRANS_OUT_MINUTES),
            stop_function=deceleration,
        )

        return transition


class NightDropAnomaly(BaseAnomaly):
    """NightDrop Anomaly."""

    ID: int = 3

    TRANS_IN_MINUTES = 60
    TRANS_OUT_MINUTES = 120

    MINUTES_MIN: int = 0
    MINUTES_MAX: int = 40
    HOURS_MIN: int = 1
    HOURS_MAX: int = 1
    DURATION_MINUTES: int = 10
    DURATION_VAR_COEFF: int = 2
    SWITCH_VAL: int = -5
    SWITCH_VAR_COEFF: int = 2

    def __init__(
        self,
        start: datetime,
        duration: timedelta,
        base_value: float = 0,
        switch_value: float = -5,
    ):
        super().__init__(
            start_time=start,
            stop_time=start + duration,
            base_value=base_value,
            switch_value=switch_value,
            transition=self._transition(),
        )

    @staticmethod
    def _sigmoid(x):
        if x == 0 or x == 1:
            return x
        return 1 / (1 + np.exp(-10 * (x - 0.5)))

    def _transition(self):
        def acceleration(x):
            return self._sigmoid(x)

        def deceleration(x):
            return 1 - (1 - x) ** 4

        # acceleration = lambda x: self.sigmoid(x)
        # deceleration = lambda x: 1 - (1 - x) ** 4

        transition = LambdaTransition(
            transition_window=timedelta(minutes=self.TRANS_IN_MINUTES),
            transition_function=acceleration,
            stop_window=timedelta(minutes=self.TRANS_OUT_MINUTES),
            stop_function=deceleration,
        )

        return transition


class ShortPeakAnomaly(BaseAnomaly):
    """ShortPeak Anomaly."""

    ID: int = 2

    TRANS_IN_MINUTES = 20
    TRANS_OUT_MINUTES = 40

    MINUTES_MIN: int = 0
    MINUTES_MAX: int = 20
    HOURS_MIN: int = 7
    HOURS_MAX: int = 8
    DURATION_MINUTES: int = 120
    DURATION_VAR_COEFF: int = 10
    SWITCH_VAL: int = 8
    SWITCH_VAR_COEFF: int = 2

    def __init__(
        self,
        start: datetime,
        duration: timedelta,
        base_value: float = 0,
        switch_value: float = 8,
    ):
        super().__init__(
            start_time=start,
            stop_time=start + duration,
            base_value=base_value,
            switch_value=switch_value,
            transition=self._transition(),
        )

    @staticmethod
    def _sigmoid(x):
        if x == 0 or x == 1:
            return x
        return 1 / (1 + np.exp(-10 * (x - 0.5)))

    def _transition(self):
        def acceleration(x):
            return 1 - (1 - x) ** 4

        def deceleration(x):
            return self._sigmoid(x)

        # acceleration = lambda x: 1 - (1 - x) ** 4
        # deceleration = lambda x: self.sigmoid(x)

        transition = LambdaTransition(
            transition_window=timedelta(minutes=self.TRANS_IN_MINUTES),
            transition_function=acceleration,
            stop_window=timedelta(minutes=self.TRANS_OUT_MINUTES),
            stop_function=deceleration,
        )
        return transition


class LongPeakAnomaly(BaseAnomaly):
    """LongPeak Anomaly."""

    ID: int = 1

    TRANS_IN_MINUTES = 120
    TRANS_OUT_MINUTES = 240

    MINUTES_MIN: int = 0
    MINUTES_MAX: int = 20
    HOURS_MIN: int = 4
    HOURS_MAX: int = 6
    DURATION_MINUTES: int = 240
    DURATION_VAR_COEFF: int = 10
    SWITCH_VAL: int = 10
    SWITCH_VAR_COEFF: int = 2

    def __init__(
        self,
        start: datetime,
        duration: timedelta,
        base_value: float = 0,
        switch_value: float = 10,
    ):
        super().__init__(
            start_time=start,
            stop_time=start + duration,
            base_value=base_value,
            switch_value=switch_value,
            transition=self._transition(),
        )

    @staticmethod
    def _sigmoid(x):
        if x == 0 or x == 1:
            return x
        return 1 / (1 + np.exp(-10 * (x - 0.5)))

    def _transition(self):
        def acceleration(x):
            return 1 - (1 - x) ** 4

        def deceleration(x):
            return self._sigmoid(x)

        # acceleration = lambda x: 1 - (1 - x) ** 4
        # deceleration = lambda x: self.sigmoid(x)

        transition = LambdaTransition(
            transition_window=timedelta(minutes=self.TRANS_IN_MINUTES),
            transition_function=acceleration,
            stop_window=timedelta(minutes=self.TRANS_OUT_MINUTES),
            stop_function=deceleration,
        )
        return transition


class NoisyDayDropAnomaly(BaseAnomaly):
    """NoisyDayDrop Anomaly."""

    ID: int = 4

    TRANS_IN_MINUTES = 0
    TRANS_OUT_MINUTES = 0

    MINUTES_MIN: int = 0
    MINUTES_MAX: int = 30
    HOURS_MIN: int = 13
    HOURS_MAX: int = 15
    DURATION_MINUTES: int = 60
    DURATION_VAR_COEFF: int = 10
    SWITCH_VAL: int = -5
    SWITCH_VAR_COEFF: int = 1

    def __init__(
        self,
        start: datetime,
        duration: timedelta,
        base_value: float = 0,
        switch_value: float = -5,
    ):
        noise = GaussianNoise()
        super().__init__(
            start_time=start,
            stop_time=start + duration,
            base_value=base_value,
            switch_value=switch_value,
        )
        self = self + noise

    def _sample_at(self, time_points: np.ndarray):
        noise = GaussianNoise()
        mask = self.transition.get_mask(
            time_points=time_points,
            start_time=self.start_time,
            stop_time=self.stop_time,
        )
        switch_with_transition = (
            self.switch_value * mask
            + 0.1
            * mask
            * np.random.normal(loc=noise.mean, scale=noise.std, size=len(time_points))
            + self.base_value * (1 - mask)
        )

        return switch_with_transition


class SyntheticEdenIssICSGenerator:
    """SyntheticEdenIss ICS Generator."""

    ANOMALY_TYPES = [
        LongPeakAnomaly,
        ShortPeakAnomaly,
        NightDropAnomaly,
        NoisyDayDropAnomaly,
        NightPeakAnomaly,
        CooldownPeak,
    ]

    CONSTRAINTS = {
        timedelta(hours=0): 20,
        timedelta(hours=4): 20,
        timedelta(hours=4): 20,
        timedelta(hours=4, minutes=40): 27,
        timedelta(hours=5): 27.3,
        timedelta(hours=5, minutes=15): 29.7,
        timedelta(hours=5, minutes=30): 30.5,
        timedelta(hours=5, minutes=45): 30,
        timedelta(hours=21, minutes=30): 30,
        timedelta(hours=21, minutes=45): 29.7,
        timedelta(hours=22): 26.5,
        timedelta(hours=22, minutes=5): 26,
        timedelta(hours=22, minutes=10): 26,
        timedelta(hours=22, minutes=30): 22,
    }

    def __init__(
        self,
        dim: int,
        n_days: int,
        anomaly_types: list[BaseAnomaly],
        n_anomalies: Union[int, list[int]],
        p_unaligned: float,
        granularity: timedelta = None,
        lags=None,
        add_red_noise=True,
    ):
        self.dim = dim
        self.n_days = n_days
        self.anomaly_types = anomaly_types
        self.n_anomalies = dict(
            zip(
                anomaly_types,
                (
                    [n_anomalies] * len(anomaly_types)
                    if isinstance(n_anomalies, int)
                    else n_anomalies
                ),
            )
        )
        self.p_unaligned = p_unaligned
        self.__sanitize_p_unaligned()
        self.granularity = (
            granularity if granularity is not None else timedelta(minutes=5)
        )
        self.lags = {} if lags is None else lags
        self.add_red_noise = add_red_noise

    def generate(self) -> tuple[list, list, list]:
        """Genrate synthetic EDEN ISS - ICS time series.

        Returns
        -------
        time_series : 3D np.ndarray, shape = (n_cases, n_channels, n_timepoints)
            The generated multivariate time series given as a 3D numpy array
        anomalies : List[List[Tuple[int, int]]]
            The start and end indices of the anomalies given as one List of Tuples
            per dimensions.
        labels : List[List[int]]
            The anomaly labels given as a List of Integers per dimension.
        """
        timeseries = []
        anomalies = []
        for i in range(self.dim):
            if self.lags is not None and i in self.lags:
                timeseries.append(
                    DailySeasonality(
                        self.CONSTRAINTS, utc_offset=timedelta(minutes=self.lags[i])
                    )
                )
            else:
                timeseries.append(DailySeasonality(self.CONSTRAINTS))
            anomalies.append(FlatTrend(0))

        # n_anomalies = dict(zip(self.anomaly_types, self.n_anomalies))
        timeseries, anomalies = self.__add_anomalies(timeseries, anomalies)

        if self.add_red_noise:
            for i, _ts in enumerate(timeseries):
                timeseries[i] += RedNoise(mean=0, std=0.5, correlation=0.5)

        return self.__generate_time_series(timeseries, anomalies)

    def __sanitize_p_unaligned(self):
        """Sanitize the value of `p_unaligned`.

        The value for `p_unaligned` needs to be sanitized for dimensions > 3 to
        keep the desired probability in the result.
        """
        if self.dim > 3:
            self.p_unaligned = min(np.sqrt(self.p_unaligned), 1)

    def __add_anomalies(
        self,
        timeseries: list[mockseries.signal.signal.Signal],
        anomalies: list[mockseries.signal.signal.Signal],
    ):
        used_days = []
        for at in self.anomaly_types:
            p_unaligned = (
                at.P_UNALIGNED if self.p_unaligned is None else self.p_unaligned
            )
            i = 1
            while i < self.n_anomalies[at] + 1:
                day = self.__sample_day(self.n_days, used_days)
                start_date = datetime(2021, 1, 1, 0, 0, 0) + timedelta(days=day)
                any_anomaly_added_on_day = False
                if p_unaligned == 1:
                    d = np.random.randint(0, self.dim)
                    timeseries[d], anomaly_switch = at.add_to_signal(
                        timeseries[d], start_date
                    )
                    anomalies[d] += anomaly_switch
                    any_anomaly_added_on_day = True
                else:
                    for d, ts in enumerate(timeseries):
                        p = np.random.rand()
                        if p < p_unaligned:
                            continue
                        timeseries[d], anomaly_switch = at.add_to_signal(ts, start_date)
                        anomalies[d] += anomaly_switch
                        any_anomaly_added_on_day = True
                if any_anomaly_added_on_day:
                    used_days.append(day)
                    i += 1

        return timeseries, anomalies

    def __sample_day(self, max_days: int = 1, used_days: list[int] = None):
        if used_days is None:
            used_days = []
        day = np.random.randint(1, max_days)
        while day in used_days:
            day = np.random.randint(1, max_days)

        return day

    def __generate_time_series(self, ts_generators, anomalies_generators):
        start_date = datetime(2021, 1, 1)
        time_points = datetime_range(
            granularity=self.granularity,
            start_time=start_date,
            end_time=start_date + timedelta(days=self.n_days),
        )

        all_time_series = []
        all_anomalies = []
        all_anomaly_labels = []
        for i, ts in enumerate(ts_generators):
            anomaly_labels = anomalies_generators[i].generate(time_points=time_points)
            anomalies = []
            labels = []
            for t in range(1, int(max(anomaly_labels) + 1)):
                anomalies_t = anomalies_from_labels(
                    (anomaly_labels == t).reshape(-1, 1)
                )
                labels += [t for _ in anomalies_t]
                anomalies += anomalies_t
            all_time_series.append(np.array([ts.generate(time_points=time_points)]).T)
            all_anomalies.append(anomalies)
            all_anomaly_labels.append(labels)

        return (
            np.moveaxis(np.array(all_time_series), (0, 1), (1, 2)),
            all_anomalies,
            all_anomaly_labels,
        )


class SyntheticEdenIssDataset:
    """Synthetic EDEN ISS - ICS dataset."""

    def __init__(
        self,
        dim: list[int],
        n_days: list[int],
        anomaly_types: list[list[BaseAnomaly]],
        n_anomalies: Union[list[int], list[list[int]]],
        r_sync: list[float],
        granularity: timedelta = None,
        lags: list[dict[int, int]] = None,
        add_red_noise: bool = True,
    ):
        self.dim = dim
        self.n_days = n_days
        self.anomaly_types = anomaly_types
        self.n_anomalies = (
            [n_anomalies] * len(anomaly_types)
            if isinstance(n_anomalies, int)
            else n_anomalies
        )
        self.r_sync = r_sync
        self.granularity = (
            granularity if granularity is not None else timedelta(minutes=5)
        )
        self.lags = lags
        self.add_red_noise = add_red_noise

        # print(len(self.dim), len(self.n_days), len(self.anomaly_types),
        # len(self.n_anomalies), len(self.p_unaligned), len(self.lags))

    def __str__(self):
        """Generate a string representation for a dataset configuration."""
        return (
            f"dim{self.dim[0]}-ndays{self.n_days[0]}-nanomalies{self.n_anomalies[0]}"
            f"-rsync{self.r_sync[0]}-lags{self.lags[0]}"
        )

    @classmethod
    def generate_random(
        cls,
        n_timeseries: int,
        dim: Union[int, tuple[int, int]],
        n_days: Union[int, tuple[int, int]],
        n_classes: Union[int, tuple[int, int]],
        n_anomalies: Union[int, tuple[int, int]],
        r_sync: Union[float, tuple[float, float]],
        granularity: timedelta = None,
        lags: Optional[Union[dict[int, int], dict[int, tuple[int, int]]]] = None,
        add_red_noise: bool = True,
    ):
        """Generate a randomized dataset.

        Parameters
        ----------
        n_timeseries : int
        dim : int, Tuple[int, int]
        n_days : int, Tuple[int, int]
        n_classes : int, Tuple[int, int]
        n_anomalies : int, Tuple[int, int]
        r_sync : float, tuple[float, float]
        granularity : timedelta, default=None
        lags : dict[int, int], dict[int, tuple[int, int]], default=None
        add_red_noise : bool, default=True

        Returns
        -------
        self

        """
        n = n_timeseries
        dim_ = dim if isinstance(dim, int) else np.random.randint(dim[0], dim[1] + 1, n)
        n_days_ = (
            n_days
            if isinstance(n_days, int)
            else np.random.randint(n_days[0], n_days[1] + 1, n)
        )
        n_anomalies_ = (
            n_anomalies
            if isinstance(n_anomalies, int)
            else np.random.randint(n_anomalies[0], n_anomalies[1] + 1, n)
        )
        r_sync_ = (
            r_sync
            if isinstance(r_sync, float)
            else np.random.uniform(r_sync[0], r_sync[1], n)
        )
        anomaly_types = []
        if isinstance(n_classes, int):
            for _i in range(n):
                anomaly_types.append(random.sample(range(1, 7), n_classes))
        else:
            n_classes_ = np.random.randint(n_classes[0], n_classes[1] + 1, n)
            for i in range(n):
                anomaly_types.append(random.sample(range(1, 7), n_classes_[i]))

        if lags is not None and isinstance(list(lags.values())[0], tuple):
            lags_ = []
            for _i in range(n):
                lags_.append(
                    {
                        k: int(np.random.randint(lags[k][0], lags[k][1], 1)[0])
                        for k in lags
                    }
                )
        else:
            lags_ = [lags] * n

        return cls.generate(
            n,
            dim_,
            n_days_,
            anomaly_types,
            n_anomalies_,
            r_sync_,
            granularity=granularity,
            lags=lags_,
            add_red_noise=add_red_noise,
        )

    @classmethod
    def generate(
        cls,
        n_timeseries: int,
        dim: Union[int, list[int]],
        n_days: Union[int, list[int]],
        anomaly_types: Union[list[int], list[list[int]]],
        n_anomalies: Union[int, list[int], list[list[int]]],
        r_sync: Union[float, list[float]],
        granularity: timedelta = None,
        lags: Optional[Union[dict[int, int], list[dict[int, int]]]] = None,
        add_red_noise: bool = True,
    ):
        """Generate a static dataset.

        Parameters
        ----------
        n_timeseries : int
        dim : int, List[int]
        n_days : int, List[int]
        anomaly_types : List[int], List[List[int]]
        n_anomalies : int, List[int], List[List[int]]
        r_sync : float, List[float]
        granularity : timedelta, default=None
        lags : Dict[int, int], List[Dict[int, int]], default=None
        add_red_noise : bool, default=True

        Returns
        -------
        self

        """
        anomaly_types = list(anomaly_types)
        n = n_timeseries

        anomaly_types_resolved = []
        if len(anomaly_types) == 0 or anomaly_types is None:
            anomaly_types_resolved = [[] * n_timeseries]
        elif isinstance(anomaly_types, list) and isinstance(anomaly_types[0], int):
            anomaly_types_resolved = [
                [
                    SyntheticEdenIssICSGenerator.ANOMALY_TYPES[i - 1]
                    for i in anomaly_types
                ]
            ] * n_timeseries
        else:
            for at in anomaly_types:
                anomaly_types_resolved.append(
                    [SyntheticEdenIssICSGenerator.ANOMALY_TYPES[i - 1] for i in at]
                )
        if isinstance(n_anomalies, int):
            n_anoms = [[n_anomalies] * len(at) for at in anomaly_types_resolved]
        elif isinstance(n_anomalies, list) and isinstance(n_anomalies[0], int):
            n_anoms = [n_anomalies] * n_timeseries
        else:
            n_anoms = n_anomalies

        lags_ = [lags] * n if isinstance(lags, dict) else lags

        return cls(
            [dim] * n if isinstance(dim, int) else dim,
            [n_days] * n if isinstance(n_days, int) else n_days,
            anomaly_types_resolved,
            n_anoms,
            [r_sync] * n if isinstance(r_sync, float) else r_sync,
            granularity,
            lags=lags_,
            add_red_noise=add_red_noise,
        )

    @staticmethod
    def __load_data(
        dim: int,
        n_days: int,
        anomaly_types: list[BaseAnomaly],
        n_anomalies: Union[int, list[int]],
        r_sync: float,
        granularity: timedelta = None,
        lags: Optional[dict[int, int]] = None,
        add_red_noise: bool = True,
    ):
        generator = SyntheticEdenIssICSGenerator(
            dim,
            n_days,
            anomaly_types,
            n_anomalies,
            1 - r_sync,
            granularity,
            lags,
            add_red_noise,
        )

        return generator.generate()

    def __getitem__(self, item):
        """Return the time series with the given index."""
        return self.__load_data(
            self.dim[item],
            self.n_days[item],
            self.anomaly_types[item],
            self.n_anomalies[item],
            self.r_sync[item],
            self.granularity,
            self.lags[item],
            self.add_red_noise,
        )

    def __len__(self):
        """Return the length of the dataset.

        The length if the dataset is the number of multivariate time series
        """
        return len(self.dim)

    def __iter__(self):
        """Initialize iterator."""
        self._index = 0
        return self

    def __next__(self):
        """Return the next multivariate time series."""
        if self._index < len(self.dim):
            result = self.__load_data(
                self.dim[self._index],
                self.n_days[self._index],
                self.anomaly_types[self._index],
                self.n_anomalies[self._index],
                self.r_sync[self._index],
                self.granularity,
                self.lags[self._index],
                self.add_red_noise,
            )
            self._index += 1

            return result
        raise StopIteration
